Skip to content

feat: auto-refresh MCP tools for slow servers via async late-binding - #42802

Closed
397239396 wants to merge 8 commits into
NousResearch:mainfrom
397239396:feat/configurable-mcp-discovery-timeout
Closed

397239396 wants to merge 8 commits into
NousResearch:mainfrom
397239396:feat/configurable-mcp-discovery-timeout

Conversation

@397239396

@397239396 397239396 commented Jun 9, 2026 •

Copy link
Copy Markdown

Problem

When the TUI or CLI starts, it waits only 0.75 seconds for all MCP servers to connect. Slow servers like lark (~8s), redis (~10s), and ssh (~4s) miss this window, so their tools never get registered with the agent.

Users had to manually /reload-mcp after startup — a poor experience.

main branch has a partial fix: _schedule_mcp_late_refresh() in tui_gateway/server.py. But it has several problems:

Problems with main's _schedule_mcp_late_refresh

  1. TUI-only — lives inside server.py, CLI has no equivalent. CLI users with slow MCP servers are still stuck.

  2. No thread safety — directly mutates agent.tools and agent.valid_tool_names without any lock. The main thread reads these during tool iteration; a concurrent write mid-iteration can produce inconsistent state.

  3. No thread deduplication — spawns a new daemon thread per session. If multiple sessions are created rapidly, multiple background threads race to update the same agent.

  4. Fragile tool-change detection — compares tool counts (len(new_defs) == len(agent.tools)), not tool names. If a slow server connects with 5 tools and the agent already has 5 from other servers, the refresh is silently skipped.

  5. TUI thread not synced to mcp_startup — entry.py creates its own _mcp_discovery_thread but never publishes it to hermes_cli.mcp_startup._mcp_discovery_thread. Any shared module trying to check "is MCP discovery still running?" gets None, even though TUI discovery is active. This is a latent bug that blocks any future shared MCP infrastructure.

Solution

Shared two-phase async MCP discovery in hermes_cli/mcp_startup.py:

Phase When Behavior Blocking
1 (startup) At agent build time Wait 0.75s for fast servers, build agent with available tools 0.75s
2 (background) After agent created Daemon thread waits for discovery to complete, then auto-merges new tools into the running agent None

Changes

File Change
hermes_cli/mcp_startup.py Added spawn_late_mcp_refresh() — shared background thread that waits for _mcp_discovery_event, then merges tools from newly-connected servers into the agent. Thread-safe (_agent_tools_lock), deduplicates threads, detects changes by tool name not count.
hermes_cli/cli_agent_setup_mixin.py Call spawn_late_mcp_refresh(agent) after CLI agent creation — CLI now also gets late-bound tools.
tui_gateway/server.py Replace TUI-local _schedule_mcp_late_refresh with shared spawn_late_mcp_refresh + on_refreshed callback that emits session.info. Removes ~60 lines of duplicate TUI-only logic.
tui_gateway/entry.py Bug fix: Sync TUI's _mcp_discovery_thread to mcp_startup._mcp_discovery_thread so shared code can see TUI discovery state. Without this, spawn_late_mcp_refresh would skip the background path and only do an inline check.
tests/hermes_cli/test_mcp_startup.py 183 lines of new tests: inline refresh, background thread, dedup, timeout, callback failure.
tests/test_tui_mcp_late_refresh.py Deleted — replaced by the shared tests above.

Why this is better than main's approach

Aspect main (_schedule_mcp_late_refresh) This PR (spawn_late_mcp_refresh)
Scope TUI only CLI + TUI (shared)
Thread safety No lock on agent.tools _agent_tools_lock guards mutations
Thread dedup None (one thread per session) Single late-refresh thread, guarded by _mcp_discovery_lock
Change detection Compares tool count Compares tool name set
Discovery-already-done No-op (skips entirely) Inline refresh check right away
Callback system Hardcoded _emit Pluggable on_refreshed callback
Entry.py sync Discovery thread invisible to shared code Bug fix: publishes thread to mcp_startup

What this replaces

  • ❌ _schedule_mcp_late_refresh in server.py — replaced by shared spawn_late_mcp_refresh
  • ❌ HERMES_MCP_DISCOVERY_TIMEOUT environment variable — no longer needed

Benefits

  • Startup is instant (~0.75s wait, no more)
  • All tools are available — slow servers auto-merge once connected, no user action needed
  • No configuration required — works out of the box
  • Thread-safe — no race conditions on tool list mutation
  • Works for both CLI and TUI — single implementation, consistent behavior
  • Tested — 6 test scenarios covering inline, background, dedup, timeout, and error paths

397239396 added 2 commits June 9, 2026 19:19
Add HERMES_MCP_DISCOVERY_TIMEOUT environment variable to allow users
with slow MCP servers (e.g. lark ~8s, redis ~10s, ssh ~4s) to increase
the discovery timeout. Default remains 0.75s for backward compatibility.

Both tui_gateway/entry.py and hermes_cli/mcp_startup.py are updated.

Resolution order:
  1. Explicit timeout argument (callers can override)
  2. HERMES_MCP_DISCOVERY_TIMEOUT env var
  3. Default 0.75s
@alt-glitch alt-glitch added type/feature New feature or request P2 Medium — degraded but workaround exists tool/mcp MCP client and OAuth comp/tui Terminal UI (ui-tui/ + tui_gateway/) comp/cli CLI entry point, hermes_cli/, setup wizard labels Jun 9, 2026
397239396 added 2 commits June 9, 2026 19:45
…ound thread

Instead of blocking startup for 15s waiting for all MCP servers to connect,
use a two-phase approach:

Phase 1 (startup): Brief 0.75s wait for fast servers, then build agent
Phase 2 (background): Spawn a daemon thread that waits for MCP discovery
to complete, then auto-refreshes the agent's tools in-place

This means:
- Startup is instant (~0.75s wait, not 15s)
- Agent starts with whatever tools were ready at build time
- Slow servers (lark ~8s, redis ~10s, ssh ~4s) auto-merge their tools
  once they connect, without user intervention
- TUI emits updated session.info so the frontend reflects new tools
- No need for HERMES_MCP_DISCOVERY_TIMEOUT env var workaround
With the late-binding async refresh mechanism, the env var is no longer
needed. Phase 1 always waits 0.75s for fast servers; slow servers are
handled by spawn_late_mcp_refresh in the background.
@397239396 397239396 changed the title feat: make MCP discovery timeout configurable via env var feat: auto-refresh MCP tools for slow servers via async late-binding Jun 9, 2026
397239396 added 3 commits June 9, 2026 22:37
- entry.py: publish discovery thread to mcp_startup module so
  spawn_late_mcp_refresh can see it
- mcp_startup.py: when discovery thread is already finished,
  do inline refresh check instead of skipping

Without these fixes, slow MCP servers (lark/redis/ssh) would
never have their tools loaded into the agent.
tui_gateway/server.py had CRLF line endings causing merge conflicts
with main. Convert back to LF.
P0 fixes:
- Remove dead 'import os' from mcp_startup.py
- Add _agent_tools_lock for thread-safe agent.tools updates
- Normalize all line endings to LF (was CRLF in 3 files)
- Add 6 unit tests for spawn_late_mcp_refresh

P1 fixes:
- Add on_refreshed callback for CLI path (user feedback)
- Guard _mcp_discovery_started = True with is_alive() check

P2 fixes:
- Reduce discovery wait timeout from 120s to 30s (_LATE_REFRESH_DISCOVERY_TIMEOUT_S)
- Move _mcp_late_refresh_thread assignment inside lock
- Extract _update_agent_tools helper for consistent tool swap
@397239396

Copy link
Copy Markdown
Author

Superseded by #48431 — resolved merge conflicts with latest main and squashed into a clean single commit.

…efresh

- Replaced main's local _schedule_mcp_late_refresh with shared spawn_late_mcp_refresh
- Removed duplicate function, using unified mcp_startup.spawn_late_mcp_refresh
- All TUI and CLI paths now use the same late-binding mechanism
@397239396 397239396 reopened this Jun 18, 2026
alt-glitch added a commit that referenced this pull request Jun 19, 2026
…binding)

A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting
after the agent's one-time tool snapshot was uncallable for the rest of the
session. The merged pre-first-turn late-refresh only helps during the dead air
before the user's first keystroke; once a turn starts it bails to protect the
prompt cache, so a user who types before the server connects never gets the
tools without a manual /reload-mcp.

Refresh the snapshot in the per-turn prologue (build_turn_context), before this
turn's first API call assembles tools=. This is cache-safe by construction: the
refresh only ever extends a fresh request prefix at a turn boundary, never
mutates the cached prefix of an in-flight turn. So late tools become callable on
the user's NEXT turn automatically, with no /reload-mcp and no cache cost.

- tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with
  no MCP servers (the common case) skip the rebuild entirely.
- agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the
  top of the prologue when MCP servers are registered.
- tests: 3 contract tests through the real build_turn_context (adds late tool;
  skipped when no servers; no snapshot churn when unchanged).

.hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety
constraint, and why the existing fixes (#48403/#41630/#42802) don't close it.
teknium1 pushed a commit that referenced this pull request Jun 19, 2026
…binding)

A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting
after the agent's one-time tool snapshot was uncallable for the rest of the
session. The merged pre-first-turn late-refresh only helps during the dead air
before the user's first keystroke; once a turn starts it bails to protect the
prompt cache, so a user who types before the server connects never gets the
tools without a manual /reload-mcp.

Refresh the snapshot in the per-turn prologue (build_turn_context), before this
turn's first API call assembles tools=. This is cache-safe by construction: the
refresh only ever extends a fresh request prefix at a turn boundary, never
mutates the cached prefix of an in-flight turn. So late tools become callable on
the user's NEXT turn automatically, with no /reload-mcp and no cache cost.

- tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with
  no MCP servers (the common case) skip the rebuild entirely.
- agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the
  top of the prologue when MCP servers are registered.
- tests: 3 contract tests through the real build_turn_context (adds late tool;
  skipped when no servers; no snapshot churn when unchanged).

.hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety
constraint, and why the existing fixes (#48403/#41630/#42802) don't close it.
gnalvesteffer pushed a commit to gnalvesteffer/hermes-agent that referenced this pull request Jun 19, 2026
…binding)

A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting
after the agent's one-time tool snapshot was uncallable for the rest of the
session. The merged pre-first-turn late-refresh only helps during the dead air
before the user's first keystroke; once a turn starts it bails to protect the
prompt cache, so a user who types before the server connects never gets the
tools without a manual /reload-mcp.

Refresh the snapshot in the per-turn prologue (build_turn_context), before this
turn's first API call assembles tools=. This is cache-safe by construction: the
refresh only ever extends a fresh request prefix at a turn boundary, never
mutates the cached prefix of an in-flight turn. So late tools become callable on
the user's NEXT turn automatically, with no /reload-mcp and no cache cost.

- tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with
  no MCP servers (the common case) skip the rebuild entirely.
- agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the
  top of the prologue when MCP servers are registered.
- tests: 3 contract tests through the real build_turn_context (adds late tool;
  skipped when no servers; no snapshot churn when unchanged).

.hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety
constraint, and why the existing fixes (NousResearch#48403/NousResearch#41630/NousResearch#42802) don't close it.
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…binding)

A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting
after the agent's one-time tool snapshot was uncallable for the rest of the
session. The merged pre-first-turn late-refresh only helps during the dead air
before the user's first keystroke; once a turn starts it bails to protect the
prompt cache, so a user who types before the server connects never gets the
tools without a manual /reload-mcp.

Refresh the snapshot in the per-turn prologue (build_turn_context), before this
turn's first API call assembles tools=. This is cache-safe by construction: the
refresh only ever extends a fresh request prefix at a turn boundary, never
mutates the cached prefix of an in-flight turn. So late tools become callable on
the user's NEXT turn automatically, with no /reload-mcp and no cache cost.

- tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with
  no MCP servers (the common case) skip the rebuild entirely.
- agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the
  top of the prologue when MCP servers are registered.
- tests: 3 contract tests through the real build_turn_context (adds late tool;
  skipped when no servers; no snapshot churn when unchanged).

.hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety
constraint, and why the existing fixes (NousResearch#48403/NousResearch#41630/NousResearch#42802) don't close it.
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…binding)

A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting
after the agent's one-time tool snapshot was uncallable for the rest of the
session. The merged pre-first-turn late-refresh only helps during the dead air
before the user's first keystroke; once a turn starts it bails to protect the
prompt cache, so a user who types before the server connects never gets the
tools without a manual /reload-mcp.

Refresh the snapshot in the per-turn prologue (build_turn_context), before this
turn's first API call assembles tools=. This is cache-safe by construction: the
refresh only ever extends a fresh request prefix at a turn boundary, never
mutates the cached prefix of an in-flight turn. So late tools become callable on
the user's NEXT turn automatically, with no /reload-mcp and no cache cost.

- tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with
  no MCP servers (the common case) skip the rebuild entirely.
- agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the
  top of the prologue when MCP servers are registered.
- tests: 3 contract tests through the real build_turn_context (adds late tool;
  skipped when no servers; no snapshot churn when unchanged).

.hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety
constraint, and why the existing fixes (NousResearch#48403/NousResearch#41630/NousResearch#42802) don't close it.
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…binding)

A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting
after the agent's one-time tool snapshot was uncallable for the rest of the
session. The merged pre-first-turn late-refresh only helps during the dead air
before the user's first keystroke; once a turn starts it bails to protect the
prompt cache, so a user who types before the server connects never gets the
tools without a manual /reload-mcp.

Refresh the snapshot in the per-turn prologue (build_turn_context), before this
turn's first API call assembles tools=. This is cache-safe by construction: the
refresh only ever extends a fresh request prefix at a turn boundary, never
mutates the cached prefix of an in-flight turn. So late tools become callable on
the user's NEXT turn automatically, with no /reload-mcp and no cache cost.

- tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with
  no MCP servers (the common case) skip the rebuild entirely.
- agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the
  top of the prologue when MCP servers are registered.
- tests: 3 contract tests through the real build_turn_context (adds late tool;
  skipped when no servers; no snapshot churn when unchanged).

.hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety
constraint, and why the existing fixes (NousResearch#48403/NousResearch#41630/NousResearch#42802) don't close it.
@teknium1

Copy link
Copy Markdown
Collaborator

Thanks for the MCP late-binding work. An automated hermes-sweeper review found that the requested behavior is already implemented on current main by a stronger, cache-safe path.

  • agent/turn_context.py:176-200 refreshes late-registered MCP tools before each turn's first API call, covering the CLI/TUI/gateway agent path without changing an in-flight cached prefix.
  • tools/mcp_tool.py:5231-5350 provides the shared name-diffed, agent-scoped, additive-preserving, generation-safe tool snapshot refresh.
  • tui_gateway/server.py:4370-4441 retains late TUI refresh only while the session is pre-first-turn; tui_gateway/entry.py:235-290 covers both stdio and desktop/dashboard discovery-thread owners.
  • This landed through 93d6e730288e4ffab8076a0539f25e37a71f238f and follow-up hardening b6e2a54a94f58f9ebafa79f45d45b0ccb2b17043 (released in v2026.6.19).

The PR's own spawn_late_mcp_refresh() lacks the current pre-turn cache gate, so it would regress the repository rule against changing toolsets mid-conversation. The contributor's note that this was superseded by #48431 is also consistent with this review.

@teknium1 teknium1 closed this Jul 14, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jul 14, 2026
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…binding)

A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting
after the agent's one-time tool snapshot was uncallable for the rest of the
session. The merged pre-first-turn late-refresh only helps during the dead air
before the user's first keystroke; once a turn starts it bails to protect the
prompt cache, so a user who types before the server connects never gets the
tools without a manual /reload-mcp.

Refresh the snapshot in the per-turn prologue (build_turn_context), before this
turn's first API call assembles tools=. This is cache-safe by construction: the
refresh only ever extends a fresh request prefix at a turn boundary, never
mutates the cached prefix of an in-flight turn. So late tools become callable on
the user's NEXT turn automatically, with no /reload-mcp and no cache cost.

- tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with
  no MCP servers (the common case) skip the rebuild entirely.
- agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the
  top of the prologue when MCP servers are registered.
- tests: 3 contract tests through the real build_turn_context (adds late tool;
  skipped when no servers; no snapshot churn when unchanged).

.hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety
constraint, and why the existing fixes (NousResearch#48403/NousResearch#41630/NousResearch#42802) don't close it.
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…binding)

A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting
after the agent's one-time tool snapshot was uncallable for the rest of the
session. The merged pre-first-turn late-refresh only helps during the dead air
before the user's first keystroke; once a turn starts it bails to protect the
prompt cache, so a user who types before the server connects never gets the
tools without a manual /reload-mcp.

Refresh the snapshot in the per-turn prologue (build_turn_context), before this
turn's first API call assembles tools=. This is cache-safe by construction: the
refresh only ever extends a fresh request prefix at a turn boundary, never
mutates the cached prefix of an in-flight turn. So late tools become callable on
the user's NEXT turn automatically, with no /reload-mcp and no cache cost.

- tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with
  no MCP servers (the common case) skip the rebuild entirely.
- agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the
  top of the prologue when MCP servers are registered.
- tests: 3 contract tests through the real build_turn_context (adds late tool;
  skipped when no servers; no snapshot churn when unchanged).

.hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety
constraint, and why the existing fixes (NousResearch#48403/NousResearch#41630/NousResearch#42802) don't close it.
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…binding)

A slow MCP server (HTTP/OAuth, 2-6s cold connect) that finishes connecting
after the agent's one-time tool snapshot was uncallable for the rest of the
session. The merged pre-first-turn late-refresh only helps during the dead air
before the user's first keystroke; once a turn starts it bails to protect the
prompt cache, so a user who types before the server connects never gets the
tools without a manual /reload-mcp.

Refresh the snapshot in the per-turn prologue (build_turn_context), before this
turn's first API call assembles tools=. This is cache-safe by construction: the
refresh only ever extends a fresh request prefix at a turn boundary, never
mutates the cached prefix of an in-flight turn. So late tools become callable on
the user's NEXT turn automatically, with no /reload-mcp and no cache cost.

- tools/mcp_tool.py: has_registered_mcp_tools() — cheap guard so sessions with
  no MCP servers (the common case) skip the rebuild entirely.
- agent/turn_context.py: call the shared refresh_agent_mcp_tools() helper at the
  top of the prologue when MCP servers are registered.
- tests: 3 contract tests through the real build_turn_context (adds late tool;
  skipped when no servers; no snapshot churn when unchanged).

.hermes/plans/: SPEC + PLAN documenting the root cause, the cache-safety
constraint, and why the existing fixes (NousResearch#48403/NousResearch#41630/NousResearch#42802) don't close it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:implemented-on-main Sweeper: behavior already present on current main tool/mcp MCP client and OAuth type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants